package lh2;

import java.net.*;
import java.io.*;
import java.util.*;
import com.thoughtworks.xstream.XStream;

import lh2.StringEncrypter.EncryptionException;

/**
 * Crea un servidor (dicho servidor puede ser nodo o raiz, donde el raíz
 * es el servidor conocido por todos. Y los nodos son el resto de los
 * servidores.
 * @author Cappetta, Vidal, Aiello
 */
public class Servidor {
    static int id;
    private static int primo=7;
    private static int base=3;
    private static NumeroSecreto numero_secreto;
    public static int puerto;
    static ServerSocket socket1;
    public static int l;
    static int s;
    static Balde balde=new Balde(5); //un balde con lugar para 5 registros
    static Hashtable<Integer, Integer> servidores=new Hashtable<Integer, Integer>();
    static int puertoRaiz=199;

    /**
     * Recibe una demanda y la ejecuta.
     * @param demanda
     * @return Lista de registros que arroja dicha demanda (por ejemplo al
     * realizar una búsqueda, los resultados de dicha búsqueda, además
     * de un mensaje informativo ("Estado")
     */
    private static List<Registro> ejecutarDemanda(Demanda dem){
       /**el primer registro del resultado indica como resulto la ejecucion
        * el resto de los registros son resultados propiamente dichos */
        List<Registro> resultado=new ArrayList<Registro>();
        if (dem.getOperacion().equals(Demanda.INSERTAR)){
            Registro reg=new Registro(dem.getClave(), dem.getCadena());
            boolean porDesbordar=balde.agregar(reg);
            Registro estado=new Registro(0, "El registro fue insertado con exito");
            resultado.add(estado);
            if(porDesbordar)
            {
                Socket conexion=conectarAServidor(Servidor.puertoRaiz);
                Demanda demDivision=new Demanda(Demanda.DESBORDE);
                Mensaje mensaje=new Mensaje(Servidor.puerto, Servidor.puertoRaiz, demDivision);
                enviar(conexion, mensaje);
                //desconectarse(conexion);
            }

        }
        else if(dem.getOperacion().equals(Demanda.ELIMINAR))
        {
            balde.eliminar(dem.getClave());
            Registro estado=new Registro(0, "El registro fue eliminado con exito");
            resultado.add(estado);
        }
        else if(dem.getOperacion().equals(Demanda.RECUPERAR))
        {
            Registro reg=balde.recuperar(dem.getClave());
            Registro estado=new Registro(0, "El registro fue recuperado con exito");
            resultado.add(estado);
            resultado.add(reg);
        }
        else if(dem.getOperacion().equals(Demanda.BUSCAR))
        {

            //LLAMADO AL SCAN!
            List<Registro> aux=Servidor.balde.scan(dem.getCadena());
            Registro estado=new Registro(0, "Los resultados del scan son: ");
            resultado.add(estado);
            for (Registro reg: aux){
                resultado.add(reg);
            }

        }
        else if(dem.getOperacion().equals(Demanda.INTEGRAR))
        {
            int idNueva=servidores.size()+1;
            int nuevoPuerto=Servidor.puertoRaiz+servidores.size()+1;
            servidores.put(nuevoPuerto, Servidor.l);
            System.out.println(servidores.toString());
            Registro estado=new Registro(0, "El servidor "+ idNueva + " esta corriendo en el puerto "+ nuevoPuerto);
            Registro reg=new Registro(idNueva, String.valueOf(nuevoPuerto));
            resultado.add(estado);
            resultado.add(reg);
        }
        else if(dem.getOperacion().equals(Demanda.DESBORDE))
        {
            int proximoADividir=proximoADividir();
            Demanda demDividirse=new Demanda(Demanda.DIVISION, proximoADividir);
            Mensaje mensaje=new Mensaje(Servidor.puerto, proximoADividir, demDividirse);
            Socket conexion=conectarAServidor(proximoADividir);
            enviar(conexion, mensaje);
            //desconectarse(conexion);
            Registro estado=new Registro(0, "Se solicita division del servidor en el puerto " + proximoADividir);
            resultado.add(estado);
        }
        else if(dem.getOperacion().equals(Demanda.DIVISION))
        {
            int puertoNuevoServ=dem.getClave();
            dividirme(puertoNuevoServ);
            Registro estado=new Registro(0, "Se va a dividir el servidor en el puerto " + puertoNuevoServ);
            resultado.add(estado);
        }
        //else if(dem.getOperacion().equals(Demanda.DIFFIEHELMAN)) {

        //}
        return resultado; /*retorno resultados en caso de una recuperacion o una busqueda por scan, en el resto de los casos
                          se devuelve la lista vacía*/
    }

    private static void dividirme(int puerto)
    {
        List<Registro> registros=balde.clonarRegistros();
        Socket conexion=conectarAServidor(puerto);
        for(Registro r : registros){
            if(lhStar(r)!=Servidor.id)
            {
                balde.eliminar(r.clave);
                Demanda dem=new Demanda(Demanda.INSERTAR, r.clave, r.campo);
                Mensaje mensaje=new Mensaje(Servidor.puerto, puerto, dem);
                enviar(conexion, mensaje);
            }
        }
        //desconectarse(conexion);
    }
    
   private static Mensaje recibir(Socket conexion){
        try {
            int character;
            InputStreamReader isr = new InputStreamReader(new BufferedInputStream(conexion.getInputStream()));
            StringBuffer process = new StringBuffer();
            while((character = isr.read()) != 13) {
              process.append((char)character);
            }
            String xml=process.toString();
            XStream xstream2 = new XStream();
            xstream2.alias("mensaje", Mensaje.class);
            Mensaje mensaje = (Mensaje)xstream2.fromXML(xml);
            desconectarse(conexion);
            return mensaje;
        }
        catch(Exception e) {
            e.printStackTrace();
            return null;
        }

   }

   private static int proximoADividir()
   {
       Enumeration<Integer> claves=servidores.keys();
       int mejorPuerto=Integer.MAX_VALUE;
       int mejorNivel=Integer.MAX_VALUE;
       while(claves.hasMoreElements())
       {
           int puerto;
           int nivel;
           puerto=claves.nextElement();
           nivel=servidores.get(puerto);
           if(nivel<mejorNivel)
           {
               if(puerto<mejorPuerto)
               {
                   mejorNivel=nivel;
                   mejorPuerto=puerto;
               }
           }
       }
       return mejorPuerto;
   }


   private static void enviar(int puerto, Mensaje mensaje)
   {
       Socket conexion=conectarAServidor(puerto);
       enviar(conexion, mensaje);
   }

   private static void enviar(Socket conexion, Mensaje mensaje) {
       try {
            OutputStreamWriter osw = new OutputStreamWriter(new BufferedOutputStream(conexion.getOutputStream()), "US-ASCII");
            XStream xstream = new XStream();
            xstream.alias("mensaje", Mensaje.class);
            String msj_marsh_xml = xstream.toXML(mensaje)+  (char) 13;
            osw.write(msj_marsh_xml);
            osw.flush();
            desconectarse(conexion);
       }
       catch(Exception e) {
           e.printStackTrace();
       }
   }

       private static Demanda configurar()
    {
        try
        {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in), 1);
            boolean salir=false;
            String entrada;

            Demanda dem=null;

            while(!salir)
            {
                System.out.print("modalidad (raiz o nodo): ");
                entrada=br.readLine();

                if(entrada.equals("raiz"))
                {
                    Servidor.id=0;
                    Servidor.puerto=Servidor.puertoRaiz;
                    servidores.put(Servidor.id, Servidor.puertoRaiz);
                    salir=true;
                }
                else if(entrada.equals("nodo"))
                {
                    Socket conexion=conectarAServidor(puertoRaiz);
                    enviar(conexion, new Mensaje(Servidor.puerto, Servidor.puertoRaiz, new Demanda(Demanda.INTEGRAR), false));
                    //desensamblo el mensaje que me envio el servidor raiz
                    Mensaje respuesta=recibir(conexion);
                    List<Registro> resultado=(List<Registro>) respuesta.getContenido();
                    //recibo la id y el puerto que me asigno el servidor raiz
                    System.out.println(resultado.get(0).campo);
                    Servidor.id=resultado.get(1).clave;
                    Servidor.puerto=Integer.parseInt(resultado.get(1).campo);
                    //desconectarse(conexion);
                    salir=true;
                }
                else
                {
                    System.out.println("No se reconoce el comando");
                }
            }
            return dem;
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return  null;
        }
    }
   private static void iniciar() {
      try {
          configurar();
          socket1 = new ServerSocket(Servidor.puerto);
          System.out.println("Servidor incializado");
      }
      catch(Exception e) {
          e.printStackTrace();
      }
   }

    private static Socket conectarAServidor(int puerto)
    {
        try
        {
            String host = "localhost";
            InetAddress address = InetAddress.getByName(host);
            Socket conexion = new Socket(address, puerto);
            return conexion;
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return null;
        }
    }

    private static void desconectarse(Socket conexion)
    {
        try{conexion.close();}catch(Exception e){e.printStackTrace();}
    }

   private static void escuchar(ServerSocket puerto) {
      try {
          while (true) {
            Socket conexion = puerto.accept();
            Mensaje mensaje=recibir(conexion);

            XStream xstream2 = new XStream();
            xstream2.alias("demanda", Demanda.class);
            //desconectarse(conexion);
            conexion=conectarAServidor(mensaje.origen);
            Mensaje respuesta;
            if (!mensaje.getEstaCifrado()){//si no esta cifrado es un mensaje de intercambio de diffie helman
                if(((Demanda)mensaje.getContenido()).getOperacion().equals(Demanda.DIFFIEHELMAN))
                {
                    System.out.println("Servidor dice: mensaje no cifrado de diffie hellman.");

                    //numero secreto, id contraparte, valor contraparte
                    int idcontraparte=mensaje.getOrigen();

                    int valorcontraparte=((Demanda)mensaje.getContenido()).getClave();
                    Random rnd = new Random();
                    int numeroaleatorio = Math.abs(rnd.nextInt()%10);//achicamos el numero aleatorio entre 0 y 10, cuanto mas mejor, pero java tiene problemas con num. grandes
                    System.out.println("el numero aleatorio es "+numeroaleatorio+", numero primo: "+Servidor.primo+", numero base: "+Servidor.base);

                    int gpowbmodp=(int)(Math.pow(Servidor.base, numeroaleatorio) % Servidor.primo);
                    System.out.println("numero a enviar al cliente: "+gpowbmodp+", idcontraparte: "+idcontraparte+", valorcontraparte: "+valorcontraparte);


                    respuesta=new Mensaje(Servidor.puerto,mensaje.getOrigen(),gpowbmodp,false);

                    int clavesecreta=(int)(Math.pow(valorcontraparte,numeroaleatorio)%Servidor.primo);
                    Servidor.numero_secreto=new NumeroSecreto(clavesecreta,idcontraparte);
                    System.out.println("LLAVE EN SERVER PARA CONTRAPARTE: "+Servidor.numero_secreto.getClaveSecreta());

                    List<Registro> resultado=new ArrayList<Registro>();
                    Registro reg=new Registro(gpowbmodp,"dh");
                    resultado.add(reg);
                    respuesta=new Mensaje(Servidor.puerto, mensaje.getOrigen(), resultado,false);
                    
                    enviar(conexion, respuesta);
                }
                else
                {
                        Demanda dem=(Demanda)mensaje.getContenido();
                        List<Registro> resultado=ejecutarDemanda(dem);
                        respuesta=new Mensaje(Servidor.puerto, mensaje.getOrigen(),resultado);
                        enviar(conexion, respuesta);
                }

            }else{
                //Hay que descifrar la demanda
                String demandaencriptada=(String)mensaje.getContenido();
                System.out.println("DEMANDA ENCRIPTADA EN EL SERVER : "+demandaencriptada);
                String demandadesencriptadaxml=desencriptar(Servidor.numero_secreto.getClaveSecreta(),demandaencriptada);
                XStream xstream = new XStream();
                xstream.alias("demanda", Demanda.class);
                Demanda dem = (Demanda)xstream2.fromXML(demandadesencriptadaxml);
                System.out.println("MENSAJE DESENCRIPTADO:\n "+dem.getCadena());
                System.out.println(servidores.toString());
                int puertoCorrespondiente=servidores.get(new Integer(lhStar(dem.getClave())));
                if(puertoCorrespondiente==Servidor.puerto) //Significa que la demanda realmente me corresponde
                {
                    List<Registro> resultado=ejecutarDemanda(dem);


                    XStream xstreamr = new XStream();
                    xstreamr.alias("resultado", ArrayList.class);

                    //serializamos el mensaje, se guarda en xml, para enviar
                    String resultados_xml = xstream.toXML(resultado);
                    String resultados_encriptados=encriptar(Servidor.numero_secreto.getClaveSecreta(),resultados_xml);
                    


                    respuesta=new Mensaje(Servidor.puerto, mensaje.getOrigen(),resultados_encriptados);
                    //respuesta=new Mensaje(Servidor.puerto, mensaje.getOrigen(), resultado);
                    enviar(conexion, respuesta);
                }
                else //Si no me corresponde lo reenvio al servidor que realemnte corresponde
                {
                    //aca la demanda no importa que esté encriptada, suponemos que los servidores se encuentran en el mismo lugar fisico
                    Mensaje reenvio=new Mensaje(mensaje.getOrigen(), puertoCorrespondiente, dem);
                    Socket conexionConServidor=conectarAServidor(puertoCorrespondiente);
                    enviar(conexionConServidor, reenvio);
                    //desconectarse(conexionConServidor);
                    Actualizacion act=new Actualizacion(servidores, l, s);
                    
                    XStream xstream2b = new XStream();
                    xstream2b.alias("actualizacion", Actualizacion.class);
                    String actualizacion_xml=xstream2b.toXML(act);
                    Demanda actualizarse=new Demanda(Demanda.ACTUALIZAR,actualizacion_xml);
                    Mensaje mensajeactualizar=new Mensaje(Servidor.puerto, mensaje.getOrigen(), actualizarse);
                    enviar(conexion, mensajeactualizar);
                }

                

            }
            //System.out.println(conexion.getInetAddress().toString());
            //conexion.connect(new InetSocketAddress("localhost", mensaje.getOrigen()));
            //System.out.println(conexion.getInetAddress().toString());
            
           //desconectarse(conexion);
          }
      }catch(Exception e) {
          e.printStackTrace();
      }
   }

    public static int lhStar(Registro r){
            int clave=r.clave;
            return lhStar(clave);
    }

    public static int lhStar(int clave)
    {
        int lh=clave % ((int)Math.pow(2, l));
        return lh;
    }




    public static String encriptar(String clave, String aencriptar) throws EncryptionException{

        String cadenaAEncriptar = aencriptar;
        String esquema = StringEncrypter.DESEDE_ENCRYPTION_SCHEME;
        StringEncrypter encriptador = new StringEncrypter( esquema, clave);
        String cadenaEncriptada = encriptador.encrypt( cadenaAEncriptar );
        return cadenaEncriptada;
    }
    public static String desencriptar(String clave, String adesencriptar) throws EncryptionException{

        //desencriptado
        String llaved = clave;
        String esquemad = StringEncrypter.DESEDE_ENCRYPTION_SCHEME;
        StringEncrypter encriptador = new StringEncrypter(esquemad,llaved);
        String desencriptado = encriptador.decrypt(adesencriptar);
        return desencriptado;
    }

   
   public static void main(String[] args) {
        iniciar();
        escuchar(socket1);
   }


}
